package com.hm.health.records.ecgpatch;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;

import androidx.annotation.NonNull;

import com.goodix.iot.libbase.utils.ArrayUtil;
import com.goodix.utils.EncryptUtil;
import com.hm.health.MyApplication;
import com.hm.health.entity.dao.EcgPatchCsvInfo;
import com.hm.health.entity.dao.EcgPatchSectorInfo;

import org.litepal.crud.DataSupport;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.LinkedBlockingQueue;

public class EcgPatchWoker {

    private final static String TAG = "EcgPatchWoker";

    private final static int CSV_TIME_SIZE = 30 * 60; // 30 minutes

    private final static int MSG_NEW_SECTOR = 1000;
    private final static int MSG_NEW_CSV = MSG_NEW_SECTOR + 1;
    private final static int MSG_SCAN_CSV = MSG_NEW_CSV + 1;

    private static EcgPatchWoker mWorker;

    private LinkedBlockingQueue<EcgPatchSector> mSectors;

    private LinkedBlockingQueue<EcgPatchCsv> mCsvs;

    private Vector<EcgPatchSectorInfo> mSavedSectorInfos;

    private Vector<EcgPatchCsvInfo> mSavedCsvInfos;

    private boolean mQuit = false;

    private WorkHandler mHandler = null;

    private EcgPatchWorkThread mThread = null;

    private Looper mLooper = null;

    private int mSampleRate = 500;

    private int mSectorNumInOneCsv = 8;

    private boolean mCsvSizeConfirm = false;

    private boolean mEncryption = false;

    private EcgPatchRecord.SyncSectorCallback mCallback = null;

    private List<Integer> mLostSectIds = null;

    private int mMaxLostSectorNum = 0;

    private int mMaxSectorId = 0;

    private int mMinSectorId = 0;

    private boolean lastQueryLost = false;

    public synchronized List<Integer> startQueryLostSectors(EcgPatchRecord.SyncSectorCallback callback, int maxSectorId, int minSectorId) {
        mCallback = callback;
        mMaxSectorId = maxSectorId;
        mMinSectorId = minSectorId; // min id is 1 by default
        lastQueryLost = false;

        List<Integer> result = getLostSectIds(maxSectorId, minSectorId);
        mMaxLostSectorNum = result.size();
        Log.d(TAG, "finish to scan in [" + minSectorId + "," + maxSectorId + "], result:" + result.toString());

        if (result.size() == 0) {
            // no more sectors, try scan csv
            scanCsv();
        }

        return result;
    }

    public void stopQueryLostSectors() {
        Log.d(TAG, "stop query sector");

        synchronized (this) {
            mCallback = null;
            // bug: cause last csv cannot be merged, if set mMaxSectorId = 0
            //mMaxSectorId = 0;

            // merge all sectors to csv, even though sector miss
            lastQueryLost = true;
            scanSectorsForCsv();
        }

        // wait until timeout
        long startTime = System.currentTimeMillis();
        while (!checkSaveHandlerFree()) {
            long nowTime = System.currentTimeMillis();
            if (nowTime - startTime > 5000) {
                Log.e(TAG, "Wait save file thread finish timeout");
                break;
            }

            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized List<Integer> getLostSectIds(int maxSectorId, int minSectorId) {
        int sectorSize = mSavedSectorInfos.size();
        int preId = 0;
        int curId = 0;
        int gap = 0;

        Collections.sort(mSavedSectorInfos, new SectorInfoCompare());
        if (mSavedSectorInfos.size() > 0) {
            Log.d(TAG, "finish to scan, mem min sector id " + mSavedSectorInfos.get(0).getSectorId() + ", max sector id " + mSavedSectorInfos.get(mSavedSectorInfos.size() - 1).getSectorId());
        }
        
        // 1. clear mLostSectIds
        mLostSectIds.clear();

        if (sectorSize == 0) {
            for(int i = 1; i <= maxSectorId; i++) {
                mLostSectIds.add(i);
            }
            Log.d(TAG, "finish to scan, all data are missed, size " + mLostSectIds.size());
            return mLostSectIds;
        }

        int minIdInSector = mSavedSectorInfos.get(0).getSectorId();
        for(int i = minSectorId; i < minIdInSector; i++) {
            mLostSectIds.add(i);
        }

        for (int i = 1; i < sectorSize; i++) {

            preId = mSavedSectorInfos.get(i -1).getSectorId();
            curId = mSavedSectorInfos.get(i).getSectorId();

            gap = curId - preId;
            int j = 0;

            if (gap == 1) {
                continue;
            } else if (gap == 0) {
                Log.e(TAG, "sector id is the same, there must be error");
                break;
            } else {

                for(j = preId + 1; j < curId; j++) {

                    if (j > maxSectorId) {
                        Log.d(TAG, "finish to scan, lostIdList size:" + mLostSectIds.size() + ", curId:" + curId + " > maxSectorId:" + maxSectorId + " i:" + i + ", sectorSize:" + sectorSize);
                        return mLostSectIds;
                    }

                    if (j < minSectorId) {
                        Log.d(TAG, "flash already lost sector " + j);
                        continue;
                    }

                    mLostSectIds.add(j);
                }
            }
        }

        int maxIdInSector = mSavedSectorInfos.get(sectorSize - 1).getSectorId();
        if (maxIdInSector < maxSectorId) {
            for(int i = maxIdInSector + 1; i <= maxSectorId; i++) {
                mLostSectIds.add(i);
            }
        }

        Log.d(TAG, "finish to scan, lostIdList size:" + mLostSectIds.size());
        return mLostSectIds;
    }

    public synchronized void addSectors(EcgPatchSector sector) {
        // TODO: debug
        /*int id = sector.getSectorInfo().getSectorId();
        if (id == 15 || id == 18 || id == 23) {
            return;
        }*/

        Log.d(TAG, "[EcgPatchData] add a sector, id:" + sector.getSectorInfo().getSectorId());
        mSectors.add(sector);
        calculateSectorsInCsv(sector.getFrameTotalNum());

        if (mHandler != null) {
            mHandler.removeMessages(MSG_NEW_SECTOR);
            mHandler.sendEmptyMessage(MSG_NEW_SECTOR);
        } else {
            Log.e(TAG, "receive a sector, but handler is null");
        }
    }

    public synchronized void scanCsv() {
        Log.d(TAG, "[EcgPatchData] scan csv, sector size " + mSectors.size());

        /*if (mHandler != null) {
            mHandler.removeMessages(MSG_SCAN_CSV);
            mHandler.sendEmptyMessage(MSG_SCAN_CSV);
        } else {
            Log.e(TAG, "try scan csv, but handler is null");
        }*/
        scanSectorsForCsv();
        checkFinishCallback();
    }

    private boolean checkSaveHandlerFree() {
        if (mHandler != null) {
            if (mHandler.hasMessages(MSG_NEW_SECTOR) || mHandler.hasMessages(MSG_NEW_CSV) || mHandler.hasMessages(MSG_SCAN_CSV)) {
                Log.d(TAG, "Handler remain msg to process");
                return false;
            }
        }
        return true;
    }

    private void checkFinishCallback() {
        synchronized (this) {
            if (mCallback != null && mMaxLostSectorNum > 0) {
                if (mLostSectIds.size() == 0) {
                    if (!checkSaveHandlerFree()) {
                        return;
                    }
                    Log.d(TAG, "All files saved");
                    mCallback.onSaveFinish();
                }
            }
        }
    }

    private int scanSectorsForCsv() {

        Log.e(TAG, "scanSectorsForCsv, saved sector size:" + mSavedSectorInfos.size());
        Log.d(TAG, "mMaxSectorId id:" + mMaxSectorId + ", mMinSectorId id:" + mMinSectorId);

        if (mSectorNumInOneCsv <0) {
            Log.e(TAG, "mSectorNumInOneCsv <= 0, it's not permit");
            return 0;
        }

        Comparator sectorComparator = new SectorInfoCompare();
        Collections.sort(mSavedSectorInfos, sectorComparator);

        int begin = 0;
        int end = 0;
        int baseSectorId = 0;
        boolean newCsv = true;
        int csvId = 0;
        int remain = 1;

        for(int i = 0; i < mSavedSectorInfos.size(); i++) {

            baseSectorId = mSavedSectorInfos.get(i).getSectorId();

            if (mSectorNumInOneCsv == 1) {
                remain = 0;
            }
            if (lastQueryLost) {
                // last query, every sector can be merge
            } else {
                // check sector id % 10 == 1
                if (baseSectorId % mSectorNumInOneCsv != remain) {
                    //Log.d(TAG, "baseSectorId:" + baseSectorId + " % mSectorNumInOneCsv:" + mSectorNumInOneCsv + "=" + (baseSectorId % mSectorNumInOneCsv) + " != " + remain);
                    continue;
                }
            }

            csvId = baseSectorId / mSectorNumInOneCsv + remain;

            begin = i;
            end = i + mSectorNumInOneCsv;
            newCsv = false;

            List<EcgPatchSectorInfo> infos = new ArrayList<>();
            infos.add(mSavedSectorInfos.get(begin));
            EcgPatchSectorInfo firstSector = infos.get(0);

            if (firstSector.isRegroupCSV()) {
//                i = i + (end - begin) - 1;
                //Log.d(TAG, "begin sector was regrouped id:" + firstSector.getSectorId());
                continue;
            }

            if (!firstSector.isValid()) {
                Log.e(TAG, "sector id = " + firstSector.getSectorId() + " is invalid, continue.");
                continue;
            }

            Log.d(TAG, "try to merge csv " + csvId + " with first sector " + firstSector.getSectorId() + ", range [" + begin + ", " + end + "], last query " + lastQueryLost);

            int maxSectorIdInRange = ((firstSector.getSectorId() + mSectorNumInOneCsv - 1) / mSectorNumInOneCsv) * mSectorNumInOneCsv;
            int j = 0;
            int prevSectorId = firstSector.getSectorId();
            for(j = begin + 1; j < end; j++) {
                if (j >= mSavedSectorInfos.size()) {
                    break;
                }

                if(mSavedSectorInfos.get(j).isRegroupCSV()) {
                    newCsv = true;
                    break;
                }

                if (!mSavedSectorInfos.get(j).isValid()) {
                    Log.e(TAG, "sector id = " + mSavedSectorInfos.get(j).getSectorId() + " is invalid, break.");
                    newCsv = true;
                    break;
                }

                int sectorId = mSavedSectorInfos.get(j).getSectorId();
                if (sectorId != prevSectorId + 1) {
                    // sector miss
                    if (lastQueryLost) {
                        // split into two csv
                        Log.w(TAG, "The next sector of " + prevSectorId + " is miss, merge once");
                        newCsv = true;
                        break;
                    } else {
                        Log.d(TAG, "The current sector id " + sectorId + " is not continuous with the previous sector id " + prevSectorId);
                        break;
                    }
                }

                /*if (sectorId > maxSectorIdInRange) {
                    newCsv = true;
                    break;
                }*/

                infos.add(mSavedSectorInfos.get(j));
                prevSectorId = sectorId;

                /*if (j == end - 1) {
                    newCsv = true;
                    break;
                }*/

                if (sectorId == maxSectorIdInRange) {
                    newCsv = true;
                    break;
                }

                if (mMaxSectorId > 0 && mMaxSectorId == sectorId) {
                    // save last sectors to csv
                    newCsv = true;
                    break;
                }
            }

            if (lastQueryLost) {
                // even though not reach max sector id
                int sectorCount = mSavedSectorInfos.size();
                if (i >= sectorCount - 1 || j >= sectorCount - 1) {
                    Log.d(TAG, "the last csv to merge");
                    newCsv = true;
                }
            }

            if (newCsv) {
                EcgPatchSectorInfo lastSector = infos.get(infos.size() - 1);
                Log.i(TAG, "[EcgPatchData] new csv id:" + csvId + ", first sector " + firstSector.getSectorId()+ ", last sector " + lastSector.getSectorId());
                EcgPatchCsv csv = new EcgPatchCsv(csvId, firstSector.getBeginTimeStamp(), lastSector.getEndTimeStamp(), firstSector.getSectorId(), lastSector.getSectorId(), firstSector.getTestId(), infos);
                addCsvs(csv);
                i = i + infos.size() - 1;
            }
        }
        Log.d(TAG, "scanSectorsForCsv newCsv:" + newCsv);
        return 0;
    }

    public synchronized void addCsvs(EcgPatchCsv csv) {

        Log.d(TAG, "[EcgPatchData] add a csv, id:" + csv.getCsvInfo().getCsvId());
        mCsvs.add(csv);

        Log.d(TAG, "added csv count " + mCsvs.size());

        if (mHandler != null) {
            //mHandler.removeMessages(MSG_NEW_CSV);
            mHandler.sendEmptyMessage(MSG_NEW_CSV);
        }
    }

    /**
     * 根据一个csv需要存数据的时间长度，以及一个sector容纳的数据时间长度，计算一个csv大概需要几个sector
     */
    private void calculateSectorsInCsv(int sectorSize) {
        if (mCsvSizeConfirm) {
            return;
        }

        if (sectorSize == 0) {
            sectorSize = 8000;
        }
        mSectorNumInOneCsv = CSV_TIME_SIZE * mSampleRate / sectorSize;
        mCsvSizeConfirm = true;
//        mSectorNumInOneCsv = 20; // TODO: debug
    }

    public EcgPatchWoker(String testId, int sampleRate) {
        mSectors = new LinkedBlockingQueue<>();
        mCsvs = new LinkedBlockingQueue<>();
        mSavedSectorInfos = new Vector<>();
        mSavedCsvInfos = new Vector<>();
        mSampleRate = sampleRate;
        mLostSectIds = new ArrayList<>();

        initFromDB(testId);

        mEncryption = MyApplication.getHbdOptionIni().isRawDataEncrypted();
        mThread = new EcgPatchWorkThread();
        mThread.start();
        Log.i(TAG, "EcgPatchWoker construct, start EcgPatchWorkThread");
    }

    private void initFromDB(String testId) {
        Log.d(TAG, "initFromDB:" + testId);
        List<EcgPatchSectorInfo> infos = DataSupport.where("testId = ?", testId).order("sectorId asc").find(EcgPatchSectorInfo.class);
        if (infos != null && infos.size() > 0) {
            for(int i = 0; i < infos.size(); i++) {
                mSavedSectorInfos.add(infos.get(i));
            }
            Log.d(TAG, "load sector info from db, size:" + mSavedSectorInfos.size());
        }

        List<EcgPatchCsvInfo> csvInfos = DataSupport.where("testId = ?", testId).order("csvId asc").find(EcgPatchCsvInfo.class);
        if (csvInfos != null && csvInfos.size() > 0) {
            for(int i = 0; i < csvInfos.size(); i++) {
                mSavedCsvInfos.add(csvInfos.get(i));
            }
            Log.d(TAG, "load csv info from db, size:" + mSavedCsvInfos.size());
        }
    }

    private void processSectors() throws IOException {

        Log.d(TAG, "processSectors, sector list size:" + mSectors.size());
        EcgPatchSector sector = null;
        int num = Math.min(mSectors.size(), mSectorNumInOneCsv);
        for(int i = 0; i < num; i++) {
            sector = mSectors.poll();
            processOneSector(sector);
        }
    }

    private void serializeRawdata(EcgPatchSector sector) throws IOException {

        File rawdataFile = new File(sector.getSectorInfo().getSectorFilePath());
        File rawdataPath = rawdataFile.getParentFile();
        if (!rawdataPath.exists()) {
            rawdataPath.mkdirs();
        }
        boolean isCreate = rawdataFile.createNewFile();
        Log.d(TAG, "serializeRawdata create new file:" + sector.getSectorInfo().getSectorFilePath() + " return:" + isCreate);
        FileOutputStream outputStream = new FileOutputStream(rawdataFile);

        // 1. save rawdata header and description
        StringBuilder builder = new StringBuilder();
        builder.append(sector.getSectorHeader().trim()).append("\n");
        if (mEncryption) {
            builder.append(EncryptUtil.encrypt(sector.getSectorColumnDescription().trim())).append("\n");
        } else {
            builder.append(sector.getSectorColumnDescription()).append("\n");
        }
        outputStream.write(builder.toString().getBytes());

        // 2. save rawdata
        builder = new StringBuilder();
        String raw = null;
        List<EcgPatchSector.EcgPatchFrame> list = sector.getFrames();
        EcgPatchSector.EcgPatchFrame frame = null;
        StringBuilder frameBuilder = null;

        for(int i = 0; i < list.size(); i++) {

            frame = list.get(i);
            frameBuilder = new StringBuilder();
            frameBuilder.append(frame.mTimestamp).append(",")
                    .append(frame.mMode).append(",")
                    .append(frame.mSectorId).append(",")
                    .append(ArrayUtil.toString(frame.mRawdata));
            raw = frameBuilder.toString();
            if (mEncryption) {
                raw = EncryptUtil.encrypt(raw);
            }
            builder.append(raw).append("\n");

            if (i % 10 == 0) {
                outputStream.write(builder.toString().getBytes());
                builder = new StringBuilder();
            }
        }

        if (builder.toString().length() > 0) {
            outputStream.write(builder.toString().getBytes());
        }

        outputStream.close();
    }

    private void serialieResult(EcgPatchSector sector) throws IOException {

        File resultFile = new File(sector.getSectorInfo().getSectorResultFilePath());
        File resultPath = resultFile.getParentFile();
        if (!resultPath.exists()) {
            resultPath.mkdirs();
        }
        boolean isCreate = resultFile.createNewFile();
        Log.d(TAG, "create new file:" + sector.getSectorInfo().getSectorResultFilePath() + " return:" + isCreate);
        FileOutputStream outputStream = new FileOutputStream(resultFile);

        // 1. save result header and description
        StringBuilder builder = new StringBuilder();
        builder.append(sector.getSectorHeader().trim()).append("\n")
                .append(sector.getSectorResultColumnDescription().trim()).append("\n");
        outputStream.write(builder.toString().getBytes());

        builder = new StringBuilder();
        String raw = null;
        List<EcgPatchSector.EcgPatchFrame> list = sector.getFrames();
        EcgPatchSector.EcgPatchFrame frame = null;

        for(int i = 0; i < list.size(); i++) {
            frame = list.get(i);
            builder.append(frame.mTimestamp).append(",")
                    .append(frame.mMode).append(",")
                    .append(frame.mSectorId).append(",")
                    .append(ArrayUtil.toString(frame.mRecord)).append("\n");

            if (i % 10 == 0) {
                outputStream.write(builder.toString().getBytes());
                builder = new StringBuilder();
            }
        }

        if (builder.toString().length() > 0) {
            outputStream.write(builder.toString().getBytes());
        }

        outputStream.close();
    }

    private void processOneSector(EcgPatchSector sector) throws IOException {

        if (sector == null) {
            Log.e(TAG, "sector is null");
            return;
        }

        int sectorId = sector.getSectorInfo().getSectorId();
        for (int i=0; i<mSavedSectorInfos.size(); i++) {
            if (sectorId == mSavedSectorInfos.get(i).getSectorId()) {
                Log.w(TAG, "Already exist same sector id " + sectorId + " in list");
                return;
            }
        }

        // 1. save data in file, contains records and server
        serializeRawdata(sector);
        serialieResult(sector);
        // 2. save info in db
        sector.getSectorInfo().save();

        mSavedSectorInfos.add(sector.getSectorInfo());
        Log.i(TAG, "[EcgPatchData] save sector:" + sector.toString());

        synchronized (this) {
            if (mCallback != null && mMaxLostSectorNum > 0) {

                int id = 0;

                Integer lostSectorId = sector.getSectorInfo().getSectorId();
                if (mLostSectIds.contains(lostSectorId)) {
                    mLostSectIds.remove(lostSectorId); // important: type of lostSectorId is Integer, not int
                    id = lostSectorId;
                } else {
                    Log.e(TAG, "[EcgPatchData] save sector " + lostSectorId + " but not found in lost list ");
                }

                Log.i(TAG, "[EcgPatchData] save sector " + id + " onProgress, max lost " + mMaxLostSectorNum + ", remain lost " + mLostSectIds.size());

                if (id != 0) {
                    mCallback.onProgress(mMaxLostSectorNum, mLostSectIds.size(), id);
                }
            }
        }
    }

    private synchronized void processCsvs() {
        Log.d(TAG, "pre process csv count " + mCsvs.size());

        EcgPatchCsv csv = null;
        while (mCsvs.size() > 0) {
            csv = mCsvs.poll();
            processOneCsv(csv);
        }

        Log.d(TAG, "after process csv count " + mCsvs.size());
    }

    private void processOneCsv(EcgPatchCsv csv) {

        Log.d(TAG, "processOneCsv id:" + csv.getCsvInfo().getCsvId() + ", first sector id " + csv.getCsvInfo().getBeginSectorId());
        csv.save();
    }

    public void quit() {
        Log.d(TAG, "request to quit ecg work looper");
        mLooper.quit();
    }

    private class WorkHandler extends Handler {

        public WorkHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            Log.i(TAG, "receive a message:" + msg.toString());
            switch (msg.what) {
                case MSG_NEW_SECTOR:
                    try {
                        processSectors();
                    } catch (IOException e) {
                        Log.e(TAG, "IOException", e);
                    }
                    scanSectorsForCsv();
                    checkFinishCallback();
                    break;
                case MSG_NEW_CSV:
                    processCsvs();
                    checkFinishCallback();
                    break;
                case MSG_SCAN_CSV:
                    scanSectorsForCsv();
                    checkFinishCallback();
                    break;
                default:
                    break;
            }

        }
    }

    class EcgPatchWorkThread extends Thread {
        @Override
        public void run() {
            super.run();
            Log.d(TAG, "EcgPatchWorkThread is running");
            Looper.prepare();
            mLooper = Looper.myLooper();
            mHandler = new WorkHandler(mLooper);
            Looper.loop();
            Log.d(TAG, "EcgPatchWorkThread quit from loop");
        }
    };

    static class SectorInfoCompare implements Comparator {

        @Override
        public int compare(Object o1, Object o2) {
            EcgPatchSectorInfo info1 = (EcgPatchSectorInfo)o1;
            EcgPatchSectorInfo info2 = (EcgPatchSectorInfo)o2;

            if (info1.getSectorId() > info2.getSectorId()) {
                return 1;
            } else if (info1.getSectorId() < info2.getSectorId()) {
                return -1;
            } else {
                return 0;
            }
        }
    }

    static class CsvInfoCompare implements Comparator {

        @Override
        public int compare(Object o1, Object o2) {
            EcgPatchCsvInfo info1 = (EcgPatchCsvInfo)o1;
            EcgPatchCsvInfo info2 = (EcgPatchCsvInfo)o2;

            if (info1.getCsvId() > info2.getCsvId()) {
                return 1;
            } else if (info1.getCsvId() < info2.getCsvId()) {
                return -1;
            } else {
                return 0;
            }
        }
    }
}
